/*
 * BLLib.SingletonApplication
 * Andy Tidball
 * 
 * Project: Black Lightning General Library
 * Copyright: GNU General Public License
 */

using System;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;
using System.Windows.Forms;

namespace BLLib {
	/// <summary>
	/// Used to control an application of which only one instance can ever be running.
	/// </summary>
	/// <remarks>
	/// <para>Use this class as a replacement for Application when only one instance should ever be running.  Call its Run() and Exit() instead of those methods on Application.</para>
	/// <para>Subscribe to the ApplicationInstanceStart event to know when the user attempts to start an instance of your application.  Note that this event is also fired when you call Run to alert you that the very first instance of the application was started.  This allows you to consolidate startup logic into a single place, leaving your Main method to basically just subscribe to the event and call Run.  Instances beyond the first will be automatically killed after the event is fired so that there's only ever more than one instance of your application running while the event is being handled.</para>
	/// </remarks>
	public static class SingletonApplication {
		// constants
		private static readonly string _CommonName = Assembly.GetEntryAssembly().GetName().Name; // common name used across instances

		// member data
		private static Mutex    _Mutex    = null;  // the mutex used to check whether another instance is already running
		private static IChannel _Channel  = null;  // the channel used to communicate with the main instance
		private static Form     _Form     = null;  // the application's main form, if it has one
		private static bool     _Activate = false; // true if the main instance's form should be activated when another instance runs
		private static bool     _Exit     = false; // true if the application should exit instead of running

		/// <summary>
		/// Runs the application.
		/// </summary>
		public static void Run() {
			// check if we're the first instance or not
			if (IsFirstInstance()) {
				// get this main instance setup
				SetupMainInstance();

				// run the application
				if (!_Exit) {
					Application.Run();
				}

				// release everything we setup
				ReleaseMainInstance();
			} else {
				// defer action to the main instance and exit
				DeferToMainInstance();
			}
		}

		/// <summary>
		/// Runs the application using the given context.
		/// </summary>
		/// <param name="Context">The context to run the application with.</param>
		public static void Run(ApplicationContext Context) {
			// check if we're the first instance or not
			if (IsFirstInstance()) {
				// get this main instance setup
				SetupMainInstance();

				// run the application
				if (!_Exit) {
					Application.Run(Context);
				}

				// release everything we setup
				ReleaseMainInstance();
			} else {
				// defer action to the main instance and exit
				DeferToMainInstance();
			}
		}

		/// <summary>
		/// Runs the application using the given main form.
		/// </summary>
		/// <param name="Form">The form to use as the application's main form.</param>
		public static void Run(Form Form) {
			// check if we're the first instance or not
			if (IsFirstInstance()) {
				// get this main instance setup
				SetupMainInstance();

				// run the application
				if (!_Exit) {
					_Form = Form;
					Application.Run(_Form);
				}

				// release everything we setup
				ReleaseMainInstance();
			} else {
				// defer action to the main instance and exit
				DeferToMainInstance();
			}
		}

		/// <summary>
		/// Runs the application using the given arguments.
		/// </summary>
		/// <param name="Args">The command-line parameters used to start the application.</param>
		public static void Run(string[] Args) {
			// check if we're the first instance or not
			if (IsFirstInstance()) {
				// get this main instance setup
				SetupMainInstance(Args);

				// run the application
				if (!_Exit) {
					Application.Run();
				}

				// release everything we setup
				ReleaseMainInstance();
			} else {
				// defer action to the main instance and exit
				DeferToMainInstance(Args);
			}
		}

		/// <summary>
		/// Runs the application using the given arguments and context.
		/// </summary>
		/// <param name="Args">The command-line parameters used to start the application.</param>
		/// <param name="Context">The context to run the application with.</param>
		public static void Run(string[] Args, ApplicationContext Context) {
			// check if we're the first instance or not
			if (IsFirstInstance()) {
				// get this main instance setup
				SetupMainInstance(Args);

				// run the application
				if (!_Exit) {
					Application.Run(Context);
				}

				// release everything we setup
				ReleaseMainInstance();
			} else {
				// defer action to the main instance and exit
				DeferToMainInstance(Args);
			}
		}

		/// <summary>
		/// Runs the application using the given arguments and main form.
		/// </summary>
		/// <param name="Args">The command-line parameters used to start the application.</param>
		/// <param name="Form">The form to use as the application's main form.</param>
		public static void Run(string[] Args, Form Form) {
			Run(Args, Form, false);
		}

		/// <summary>
		/// Runs the application using the given main form and activation setting.
		/// </summary>
		/// <param name="Args">The command-line parameters used to start the application.</param>
		/// <param name="Form">The form to use as the application's main form.</param>
		/// <param name="ActivateFormOnSecondInstance">Whether or not the main form should be activated when a second instance starts.</param>
		public static void Run(string[] Args, Form Form, bool ActivateFormOnSecondInstance) {
			// check if we're the first instance or not
			if (IsFirstInstance()) {
				// get this main instance setup
				SetupMainInstance(Args);

				// run the application
				if (!_Exit) {
					_Form = Form;
					_Activate = ActivateFormOnSecondInstance;
					Application.Run(_Form);
				}

				// release everything we setup
				ReleaseMainInstance();
			} else {
				// defer action to the main instance and exit
				DeferToMainInstance(Args);
			}
		}

		/// <summary>
		/// Stops the application and exits.
		/// </summary>
		public static void Exit() {
			_Exit = true;
			Application.Exit();
		}

		/// <summary>
		/// Checks if this is the first instance of the application running or not.
		/// </summary>
		/// <returns></returns>
		private static bool IsFirstInstance() {
			// Create a new mutex object with a unique name
			// (name is prefixed with Local so that it will be per Windows-user)
			_Mutex = new Mutex(false, "Local\\" + _CommonName);
			return _Mutex.WaitOne(TimeSpan.Zero, false);
		}

		/// <summary>
		/// Sets up the main instance for cleanup and to listen for other instances trying to run.
		/// </summary>
		private static void SetupMainInstance() {
			SetupMainInstance(new string[0]);
		}

		/// <summary>
		/// Sets up the main instance for cleanup and to listen for other instances trying to run.
		/// </summary>
		/// <param name="Args">Arguments used to start the main instance.</param>
		private static void SetupMainInstance(string[] Args) {
			// create and register our channel
			_Channel = new IpcChannel(_CommonName);
			ChannelServices.RegisterChannel(_Channel, false);

			// register the listener type at its URI
			RemotingConfiguration.RegisterWellKnownServiceType(typeof(SingletonApplicationListener), SingletonApplicationListener.URI, WellKnownObjectMode.SingleCall);

			// inform the application about the first instance
			OnApplicationInstanceStart(new ApplicationInstanceStartEventArgs(Args));
		}

		/// <summary>
		/// Releases everything setup by IsFirstInstance and SetupMainInstance.
		/// </summary>
		private static void ReleaseMainInstance() {
			ChannelServices.UnregisterChannel(_Channel);
			_Mutex.ReleaseMutex();
			_Mutex.Close();
		}

		/// <summary>
		/// Sends a note to the main instance that another instance tried to start.
		/// </summary>
		private static void DeferToMainInstance() {
			DeferToMainInstance(new string[0]);
		}

		/// <summary>
		/// Sends a note to the main instance that another instance tried to start using the given arguments.
		/// </summary>
		/// <param name="Args">The arguments to pass to the main instance.</param>
		private static void DeferToMainInstance(string[] Args) {
			// construct the URI to connect to
			string URI = "ipc://" + _CommonName + "/" + SingletonApplicationListener.URI;

			// register the host type
			RemotingConfiguration.RegisterWellKnownClientType(typeof(SingletonApplicationListener), URI);

			// connect to it and create an instance
			SingletonApplicationListener SAL = (SingletonApplicationListener)RemotingServices.Connect(typeof(SingletonApplicationListener), URI);

			// send the data to it
			SAL.SendData(Args);
		}

		/// <summary>
		/// Called when a set of arguments is received from another instance.
		/// </summary>
		/// <param name="Args">The arguments received from another instance.</param>
		internal static void DataReceived(string[] Args) {
			// if the main form should be activated, do that
			if (_Form != null && _Activate) {
				_Form.Activate();
				if (_Form.WindowState == FormWindowState.Minimized) {
					_Form.WindowState = FormWindowState.Normal;
				}
			}

			// create a new instance with the received arguments
			OnApplicationInstanceStart(new ApplicationInstanceStartEventArgs(Args));
		}
		
		/// <summary>
		/// The prototype of the ApplicationInstanceStart event.
		/// </summary>
		/// <param name="Sender">The object sending the event (always null).</param>
		/// <param name="e">Arguments sent along with the event.</param>
		public delegate void ApplicationInstanceStartHandler(object Sender, ApplicationInstanceStartEventArgs e);

		/// <summary>
		/// Fired whenever a second instance of a SingletonApplication attempts to start.
		/// </summary>
		public static event ApplicationInstanceStartHandler ApplicationInstanceStart;

		/// <summary>
		/// Fires the ApplicationInstanceStart event.
		/// </summary>
		/// <param name="e"></param>
		private static void OnApplicationInstanceStart(ApplicationInstanceStartEventArgs e) {
			if (ApplicationInstanceStart != null) {
				ApplicationInstanceStart(null, e);
			}
		}
	}

	/// <summary>
	/// Listens for second run attempts by SingletonApplications and informs the main one instead.
	/// </summary>
	internal class SingletonApplicationListener : MarshalByRefObject {
		// constants
		public const string URI = "SingletonApplicationListener";

		// sends the given data to the main instance
		public void SendData(string[] Args) {
			SingletonApplication.DataReceived(Args);
		}
	}

	/// <summary>
	/// The data sent by an ApplicationInstanceStart event.
	/// </summary>
	public class ApplicationInstanceStartEventArgs : EventArgs {
		// member data
		private string[] _Args; // the command-line arguments passed to the application

		/// <summary>
		/// Creates a new set of ApplicationInstanceStartEventArgs with no command-line parameters.
		/// </summary>
		public ApplicationInstanceStartEventArgs() {
			_Args = new string[0];
		}

		/// <summary>
		/// Creates a new set of ApplicationInstanceStartEventArgs with the given command-line parameters.
		/// </summary>
		/// <param name="Args">The command-line parameters passed to the application instance.</param>
		public ApplicationInstanceStartEventArgs(string[] Args) {
			_Args = Args;
		}

		/// <summary>
		/// The command-line arguments passed to the application instance, or null if none were given.
		/// </summary>
		public string[] Args {
			get {
				return _Args;
			}
		}
	}
}
